查看原文
其他

Android系统启动之init进程(二)-「Android取经之路」

IngresGe IngresGe 2021-11-05


上一节主要讲了Init进程的整体架构,以及Kernel启动的部分内容。

Android系统启动之init进程(一)-「Android取经之路」

本节主要讲解Android第一阶段启动的相关内容。第一阶段主要有以下内容:

  • ueventd/watchdogd跳转及环境变量设置

  • 挂载文件系统并创建目录

  • 初始化日志输出、挂载分区设备

  • 启用SELinux安全策略

  • 开始第二阶段前的准备


4. Init 进程启动源码分析

我们主要是分析Android Q(10.0) 的init的代码。涉及源码文件:

platform/system/core/init/main.cpp
platform/system/core/init/init.cpp
platform/system/core/init/ueventd.cpp
platform/system/core/init/selinux.cpp
platform/system/core/init/subcontext.cpp
platform/system/core/base/logging.cpp
platform/system/core/init/first_stage_init.cpp
platform/system/core/init/first_stage_main.cpp
platform/system/core/init/first_stage_mount.cpp
platform/system/core/init/keyutils.h
platform/system/core/init/property_service.cpp
platform/external/selinux/libselinux/src/label.c
platform/system/core/init/signal_handler.cpp
platform/system/core/init/service.cpp



4.1 Init 进程入口

前面已经通过kernel_init,启动了init进程,init进程属于一个守护进程,准确的说,它是Linux系统中用户控制的第一个进程,它的进程号为1。它的生命周期贯穿整个Linux内核运行的始终。Android中所有其它的进程共同的鼻祖均为init进程。   可以通过"adb shell ps |grep init" 的命令来查看init的进程号。   Android Q(10.0) 的init入口函数由原先的init.cpp 调整到了main.cpp,把各个阶段的操作分离开来,使代码更加简洁命令,接下来我们就从main函数开始学习。

代码路径:platform/system/core/init/main.cpp

/* * 1.第一个参数argc表示参数个数,第二个参数是参数列表,也就是具体的参数 * 2.main函数有四个参数入口, *一是参数中有ueventd,进入ueventd_main *二是参数中有subcontext,进入InitLogging 和SubcontextMain *三是参数中有selinux_setup,进入SetupSelinux *四是参数中有second_stage,进入SecondStageMain *3.main的执行顺序如下: * (1)ueventd_main init进程创建子进程ueventd, * 并将创建设备节点文件的工作托付给ueventd,ueventd通过两种方式创建设备节点文件 * (2)FirstStageMain 启动第一阶段 * (3)SetupSelinux 加载selinux规则,并设置selinux日志,完成SELinux相关工作 * (4)SecondStageMain 启动第二阶段 */int main(int argc, char** argv) { //当argv[0]的内容为ueventd时,strcmp的值为0,!strcmp为1 //1表示true,也就执行ueventd_main,ueventd主要是负责设备节点的创建、权限设定等一些列工作 if (!strcmp(basename(argv[0]), "ueventd")) { return ueventd_main(argc, argv); } //当传入的参数个数大于1时,执行下面的几个操作 if (argc > 1) { //参数为subcontext,初始化日志系统, if (!strcmp(argv[1], "subcontext")) { android::base::InitLogging(argv, &android::base::KernelLogger); const BuiltinFunctionMap function_map; return SubcontextMain(argc, argv, &function_map); } //参数为“selinux_setup”,启动Selinux安全策略 if (!strcmp(argv[1], "selinux_setup")) { return SetupSelinux(argv); } //参数为“second_stage”,启动init进程第二阶段 if (!strcmp(argv[1], "second_stage")) { return SecondStageMain(argc, argv); } } // 默认启动init进程第一阶段 return FirstStageMain(argc, argv);}



4.2 ueventd_main

代码路径:platform/system/core/init/ueventd.cpp

Android根文件系统的镜像中不存在“/dev”目录,该目录是init进程启动后动态创建的。因此,建立Android中设备节点文件的重任,也落在了init进程身上。为此,init进程创建子进程ueventd,并将创建设备节点文件的工作托付给ueventd。ueventd通过两种方式创建设备节点文件。

第一种方式对应“冷插拔”(Cold Plug),即以预先定义的设备信息为基础,当ueventd启动后,统一创建设备节点文件。这一类设备节点文件也被称为静态节点文件。

第二种方式对应“热插拔”(Hot Plug),即在系统运行中,当有设备插入USB端口时,ueventd就会接收到这一事件,为插入的设备动态创建设备节点文件。这一类设备节点文件也被称为动态节点文件。


int ueventd_main(int argc, char** argv) { //设置新建文件的默认值,这个与chmod相反,这里相当于新建文件后的权限为666 umask(000); //初始化内核日志,位于节点/dev/kmsg, 此时logd、logcat进程还没有起来, //采用kernel的log系统,打开的设备节点/dev/kmsg, 那么可通过cat /dev/kmsg来获取内核log。 android::base::InitLogging(argv, &android::base::KernelLogger); //注册selinux相关的用于打印log的回调函数 SelinuxSetupKernelLogging(); SelabelInitialize(); //解析xml,根据不同SOC厂商获取不同的hardware rc文件 auto ueventd_configuration = ParseConfig({"/ueventd.rc", "/vendor/ueventd.rc", "/odm/ueventd.rc", "/ueventd." + hardware + ".rc"}); //冷启动 if (access(COLDBOOT_DONE, F_OK) != 0) { ColdBoot cold_boot(uevent_listener, uevent_handlers); cold_boot.Run(); } for (auto& uevent_handler : uevent_handlers) { uevent_handler->ColdbootDone(); } //忽略子进程终止信号 signal(SIGCHLD, SIG_IGN); // Reap and pending children that exited between the last call to waitpid() and setting SIG_IGN // for SIGCHLD above. //在最后一次调用waitpid()和为上面的sigchld设置SIG_IGN之间退出的获取和挂起的子级 while (waitpid(-1, nullptr, WNOHANG) > 0) { } //监听来自驱动的uevent,进行“热插拔”处理 uevent_listener.Poll([&uevent_handlers](const Uevent& uevent) { for (auto& uevent_handler : uevent_handlers) { uevent_handler->HandleUevent(uevent); //热启动,创建设备 } return ListenerAction::kContinue; }); return 0;}



4.3 init 进程启动第一阶段

代码路径:platform\system\core\init\first_stage_init.cpp

init进程第一阶段做的主要工作是挂载分区,创建设备节点和一些关键目录,初始化日志输出系统,启用SELinux安全策略 第一阶段完成以下内容:   

/* 01. 创建文件系统目录并挂载相关的文件系统 */    /* 02. 屏蔽标准的输入输出/初始化内核log系统 */

4.3.1  FirstStageMain

int FirstStageMain(int argc, char** argv) { //init crash时重启引导加载程序 //这个函数主要作用将各种信号量,如SIGABRT,SIGBUS等的行为设置为SA_RESTART,一旦监听到这些信号即执行重启系统 if (REBOOT_BOOTLOADER_ON_PANIC) { InstallRebootSignalHandlers(); } //清空文件权限 umask(0); CHECKCALL(clearenv()); CHECKCALL(setenv("PATH", _PATH_DEFPATH, 1)); //在RAM内存上获取基本的文件系统,剩余的被rc文件所用 CHECKCALL(mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755")); CHECKCALL(mkdir("/dev/pts", 0755)); CHECKCALL(mkdir("/dev/socket", 0755)); CHECKCALL(mount("devpts", "/dev/pts", "devpts", 0, NULL));#define MAKE_STR(x) __STRING(x) CHECKCALL(mount("proc", "/proc", "proc", 0, "hidepid=2,gid=" MAKE_STR(AID_READPROC)));#undef MAKE_STR // 非特权应用不能使用Andrlid cmdline CHECKCALL(chmod("/proc/cmdline", 0440)); gid_t groups[] = {AID_READPROC}; CHECKCALL(setgroups(arraysize(groups), groups)); CHECKCALL(mount("sysfs", "/sys", "sysfs", 0, NULL)); CHECKCALL(mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL)); CHECKCALL(mknod("/dev/kmsg", S_IFCHR | 0600, makedev(1, 11))); if constexpr (WORLD_WRITABLE_KMSG) { CHECKCALL(mknod("/dev/kmsg_debug", S_IFCHR | 0622, makedev(1, 11))); } CHECKCALL(mknod("/dev/random", S_IFCHR | 0666, makedev(1, 8))); CHECKCALL(mknod("/dev/urandom", S_IFCHR | 0666, makedev(1, 9))); //这对于日志包装器是必需的,它在ueventd运行之前被调用 CHECKCALL(mknod("/dev/ptmx", S_IFCHR | 0666, makedev(5, 2))); CHECKCALL(mknod("/dev/null", S_IFCHR | 0666, makedev(1, 3))); //在第一阶段挂在tmpfs、mnt/vendor、mount/product分区。其他的分区不需要在第一阶段加载, //只需要在第二阶段通过rc文件解析来加载。 CHECKCALL(mount("tmpfs", "/mnt", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV, "mode=0755,uid=0,gid=1000")); //创建可供读写的vendor目录 CHECKCALL(mkdir("/mnt/vendor", 0755)); // /mnt/product is used to mount product-specific partitions that can not be // part of the product partition, e.g. because they are mounted read-write. CHECKCALL(mkdir("/mnt/product", 0755)); // 挂载APEX,这在Android 10.0中特殊引入,用来解决碎片化问题,类似一种组件方式,对Treble的增强, // 不写谷歌特殊更新不需要完整升级整个系统版本,只需要像升级APK一样,进行APEX组件升级 CHECKCALL(mount("tmpfs", "/apex", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV, "mode=0755,uid=0,gid=0")); // /debug_ramdisk is used to preserve additional files from the debug ramdisk CHECKCALL(mount("tmpfs", "/debug_ramdisk", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV, "mode=0755,uid=0,gid=0"));#undef CHECKCALL //把标准输入、标准输出和标准错误重定向到空设备文件"/dev/null" SetStdioToDevNull(argv); //在/dev目录下挂载好 tmpfs 以及 kmsg //这样就可以初始化 /kernel Log 系统,供用户打印log InitKernelLogging(argv); ... /* 初始化一些必须的分区 *主要作用是去解析/proc/device-tree/firmware/android/fstab, * 然后得到"/system", "/vendor", "/odm"三个目录的挂载信息 */ if (!DoFirstStageMount()) { LOG(FATAL) << "Failed to mount required partitions early ..."; } struct stat new_root_info; if (stat("/", &new_root_info) != 0) { PLOG(ERROR) << "Could not stat(\"/\"), not freeing ramdisk"; old_root_dir.reset(); } if (old_root_dir && old_root_info.st_dev != new_root_info.st_dev) { FreeRamdisk(old_root_dir.get(), old_root_info.st_dev); } SetInitAvbVersionInRecovery(); static constexpr uint32_t kNanosecondsPerMillisecond = 1e6; uint64_t start_ms = start_time.time_since_epoch().count() / kNanosecondsPerMillisecond; setenv("INIT_STARTED_AT", std::to_string(start_ms).c_str(), 1); //启动init进程,传入参数selinux_steup // 执行命令:/system/bin/init selinux_setup const char* path = "/system/bin/init"; const char* args[] = {path, "selinux_setup", nullptr}; execv(path, const_cast<char**>(args)); PLOG(FATAL) << "execv(\"" << path << "\") failed"; return 1;  }

4.4 加载SELinux规则

SELinux是「Security-Enhanced Linux」的简称,是美国国家安全局「NSA=The National Security Agency」和SCC(Secure Computing Corporation)开发的 Linux的一个扩张强制访问控制安全模块。   

在这种访问控制体系的限制下,进程只能访问那些在他的任务中所需要文件。

selinux有两种工作模式:

  • permissive,所有的操作都被允许(即没有MAC),但是如果违反权限的话,会记录日志,一般eng模式用

  • enforcing,所有操作都会进行权限检查。一般user和user-debug模式用 不管是security_setenforce还是security_getenforce都是去操作/sys/fs/selinux/enforce 文件, 0表示permissive 1表示enforcing


4.4.1 SetupSelinux

作用:初始化selinux,加载SELinux规则,配置SELinux相关log输出,并启动第二阶段
代码路径:platform\system\core\init\selinux.cpp


/*此函数初始化selinux,然后执行init以在init selinux中运行*/int SetupSelinux(char** argv) { //初始化Kernel日志 InitKernelLogging(argv); // Debug版本init crash时重启引导加载程序 if (REBOOT_BOOTLOADER_ON_PANIC) { InstallRebootSignalHandlers(); } //注册回调,用来设置需要写入kmsg的selinux日志 SelinuxSetupKernelLogging(); //加载SELinux规则 SelinuxInitialize(); /* *我们在内核域中,希望转换到init域。在其xattrs中存储selabel的文件系统(如ext4)不需要显式restorecon, *但其他文件系统需要。尤其是对于ramdisk,如对于a/b设备的恢复映像,这是必需要做的一步。 *其实就是当前在内核域中,在加载Seliux后,需要重新执行init切换到C空间的用户态 */ if (selinux_android_restorecon("/system/bin/init", 0) == -1) { PLOG(FATAL) << "restorecon failed of /system/bin/init failed"; } //准备启动innit进程,传入参数second_stage const char* path = "/system/bin/init"; const char* args[] = {path, "second_stage", nullptr}; execv(path, const_cast<char**>(args)); /* *执行 /system/bin/init second_stage, 进入第二阶段 */ PLOG(FATAL) << "execv(\"" << path << "\") failed"; return 1;}

4.4.2 SelinuxInitialize()

/*加载selinux 规则*/void SelinuxInitialize() { LOG(INFO) << "Loading SELinux policy"; if (!LoadPolicy()) { LOG(FATAL) << "Unable to load SELinux policy"; } //获取当前Kernel的工作模式 bool kernel_enforcing = (security_getenforce() == 1); //获取工作模式的配置 bool is_enforcing = IsEnforcing(); //如果当前的工作模式与配置的不同,就将当前的工作模式改掉 if (kernel_enforcing != is_enforcing) { if (security_setenforce(is_enforcing)) { PLOG(FATAL) << "security_setenforce(" << (is_enforcing ? "true" : "false") << ") failed"; } } if (auto result = WriteFile("/sys/fs/selinux/checkreqprot", "0"); !result) { LOG(FATAL) << "Unable to write to /sys/fs/selinux/checkreqprot: " << result.error(); }}/* *加载SELinux规则 *这里区分了两种情况,这两种情况只是区分从哪里加载安全策略文件, *第一个是从 /vendor/etc/selinux/precompiled_sepolicy 读取, *第二个是从 /sepolicy 读取,他们最终都是调用selinux_android_load_policy_from_fd方法 */bool LoadPolicy() { return IsSplitPolicyDevice() ? LoadSplitPolicy() : LoadMonolithicPolicy();}


下一节主要讲解init进程启动的第二阶段,欢迎关注我。

视频 小程序 ,轻点两下取消赞 在看 ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存